import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:cache_store/src/helpers/serialization.dart';
import 'package:cache_store/src/serializers/serializer.dart';
import 'package:meta/meta.dart';

import '../../cache_store.dart';

/// A [MemoryCacheStore] that periodically save all its
/// items to the file system.
/// 
/// Before the first operation it read the content of
/// the [file] and load all its items thanks to the
/// [deserializer].
/// 
/// This store isn't recommended for data that are 
/// updated very often since all data is written from
/// the beginning each time.
class FileCacheStore<T> extends MemoryCacheStore<T> {

  final File file;
  final CacheSerializer<T> serializer;
  final Duration writeAfter;
  Future _loadFuture;
  Future _writeFuture;
  bool _isWriting = false;
  bool _isInitialized = false;

  FileCacheStore(
      {@required this.file,
      @required this.serializer,
      this.writeAfter = const Duration(seconds: 2)});

  @override
  Future<void> set(Cache<T> value) async {
    await _initialize();
    await super.set(value);
    this._planifyWrite();
  }

  @override
  Future<void> setAll(List<Cache<T>> values) async {
    await _initialize();
    await super.setAll(values);
    this._planifyWrite();
  }

  @override
  Future<void> clean(CachePriority priority) async {
    await _initialize();
    await super.clean(priority);
    this._planifyWrite();
  }

  @override
  Future<Cache<T>> read(String key) async {
    await _initialize();
    return super.read(key);
  }

  @override
  Future<List<Cache<T>>> readAll(List<String> keys) async {
    await _initialize();
    return await super.readAll(keys);
  }

  @override
  Future<void> delete(String key) async {
    await _initialize();
    await super.delete(key);
    this._planifyWrite();
  }

  Future<void> _initialize() {
    if (!_isInitialized) {
      return _loadFuture ?? (_loadFuture = _load());
    }

    return Future.value();
  }

  Future<void> _load() async {
    if(await file.exists()) {
      final bytes = await file.readAsBytes();
      final reader = _FileCacheReader(this.serializer);
      this.items.addAll(reader.read(bytes));
    }
    _isInitialized = true;
    _loadFuture = null;
  }

  Future<void> _planifyWrite() async {
    if(_writeFuture != null) {
      if(_isWriting) {
        await _writeFuture;
        _writeFuture = _write();
      }
    }
    else {
      _writeFuture = _write();
    }
  }

  Future<void> _write() async {
    await Future.delayed(this.writeAfter);

    _isWriting = true;
    final sink = this.file.openWrite();

    final all = this.items.toList();
    for (var item in all) {
      await _writeItem(sink, item);
    }

    await sink.close();

    _writeFuture = null;
    _isWriting = false;
  }

  Future<void> _writeItem(IOSink sink, Cache<T> item) async {
    final data = this.serializer.serialize(item.value);

    // Key
    final encoded = utf8.encode(item.key);
    final keyLengthBytes = intToBytes(encoded.length);
    sink.add(keyLengthBytes);
    sink.add(encoded);

    // Priority
    sink.add([item.priority.index]);

    // Expiry
    sink.add(intToBytes(item.expiry.microsecondsSinceEpoch));

    // Data
    sink.add(intToBytes(data.length));
    sink.add(data);

    await sink.flush();
  }
}

class _FileCacheReader<T> {
  final CacheSerializer<T> serializer;

  int offset = 0;

  _FileCacheReader(this.serializer);

  List<Cache<T>> read(List<int> bytes) {
    offset = 0;
    final result = List<Cache<T>>();
    while (offset < bytes.length) {
      result.add(_readItem(bytes));
    }
    return result;
  }

  Cache<T> _readItem(List<int> bytes) {
    // Key
    final keyLength = intFromBytes(bytes.skip(offset).take(8).toList());
    offset += 8;
    final key = utf8.decode(bytes.skip(offset).take(keyLength).toList());
    offset += keyLength;

    // Priority
    final priority = CachePriority.values[bytes.skip(offset).first];
    offset += 1;

    // Expiry
    final microsecondsSinceEpoch =
        intFromBytes(bytes.skip(offset).take(8).toList());
    final expiry = DateTime.fromMicrosecondsSinceEpoch(microsecondsSinceEpoch);
    offset += 8;

    // Data
    final dataLength = intFromBytes(bytes.skip(offset).take(8).toList());
    offset += 8;
    final value = this
        .serializer
        .deserialize(bytes.skip(offset).take(dataLength).toList());
    offset += dataLength;

    return Cache<T>(
      key: key,
      expiry: expiry,
      value: value.value,
      priority: priority,
    );
  }
}
